3장. 결합과 복잡성: 상호작용

출처: 『소프트웨어 설계의 결합 균형』(블라드 코노노프 지음, 장연호 옮김, 제이펍 2026) | 공식: https://www.jpub.kr/ | 원서: https://www.manning.com/books/balancing-coupling-in-software-design

"복잡성은 부분의 수나 크기에서 발생하는 것이 아니라, 부분 간의 상호작용에서 일어나고, 거기서 문제가 발생한다." — 3장 표제어. 선형적 상호작용은 간단하고 명확하지만, 복잡한 상호작용에서는 실패가 가까이에 있다.


학습 목표

이 장을 끝내면 다음을 할 수 있다.

  • 필수적 복잡성과 우발적 복잡성을 구분하고, 각각에 대한 처방을 제시한다.
  • 선형적 상호작용과 복잡한 상호작용의 차이를 판단하고, 주어진 코드/설계가 어느 쪽에 속하는지 분류한다.
  • 자유도 개념을 소프트웨어 설계에 적용하여 계산하고, 자유도가 복잡성과 어떻게 연결되는지 설명한다.
  • 지역 복잡성과 전역 복잡성의 트레이드오프를 평가하고, 양쪽을 동시에 관리해야 하는 이유를 논거를 들어 설명한다.
  • Repository 설계 세 가지(SQL 직접·Query 객체·파인더 메서드) 중 주어진 상황에서 어느 것이 적합한지 선택하고 근거를 제시한다.

전체 흐름도

[ 복잡성의 두 출처 ]
    ├── 필수적 복잡성 (도메인 자체에 내재 — 제거 불가, 관리만 가능)
    └── 우발적 복잡성 (설계 부주의 부산물 — 제거 대상)
                │
                ▼
    [ 상호작용 유형이 복잡성을 결정한다 ]
        ├── 선형적 상호작용 → 예측 가능, 인과 명확 (기계식 시계 비유)
        └── 복잡한 상호작용 → 예측 불가, 의도치 않은 결과
                │
                ▼
    [ 계층적 복잡성 — 모든 수준에서 발생 ]
        ├── 전역 복잡성 (구성요소 간 상호작용)
        └── 지역 복잡성 (단일 구성요소 내부 상호작용)
        → 한쪽만 최적화하면 다른 쪽이 폭등 (모놀리스 함정 / 분산 큰 진흙덩이)
                │
                ▼
    [ 자유도 = 독립적으로 변할 수 있는 변수의 수 ]
        자유도 ↑ → 복잡한 상호작용 ↑ → 의도치 않은 결과 ↑
        원인: 데이터 복제 / 비즈니스 로직 중복
                │
                ▼
    [ 제약 = 자유도를 줄이는 도구 ]
        불변식·비즈니스 규칙·캡슐화된 인터페이스
                │
                ▼
    [ 결합 = 구성요소 통합의 제약 조건을 설계하는 행위 ]
        원하는 상호작용은 허용, 복잡한 상호작용은 금지
        → 적절한 결합이 시스템 자유도를 줄이고 복잡성을 통제한다

0. 사전 필수 용어

  • 필수적 복잡성 (Essential Complexity) — 시스템이 해결하는 비즈니스 도메인에 내재된 복잡성. 제거 불가, 신중한 설계로 관리해야 한다.
  • 우발적 복잡성 (Accidental Complexity) — 최적화되지 않은 설계 결정의 부산물. 엔지니어가 만들어 내는 불필요한 복잡성. 제거 가능.
  • 선형적 상호작용 (Linear Interaction) — 구성요소 간 종속성이 명확하고 인과관계가 분명한 상호작용. 원인과 결과를 추적할 수 있다.
  • 복잡한 상호작용 (Complex Interaction) — 인과관계가 불분명하고 의도치 않은 효과가 발생하는 상호작용. 실험 없이 예측 불가.
  • 지역 복잡성 (Local Complexity) — 단일 구성요소 내부에서 발생하는 상호작용의 복잡성.
  • 전역 복잡성 (Global Complexity) — 여러 구성요소 사이의 상호작용에서 발생하는 복잡성.
  • 자유도 (Degrees of Freedom) — 다른 변수의 제약을 받지 않고 독립적으로 변할 수 있는 변수의 수. 자유도가 높을수록 가능한 시스템 상태가 많아진다.
  • 제약 (Constraint) — 구성요소가 가질 수 있는 상태나 수행할 수 있는 상호작용을 제한하는 규칙. 불변식·비즈니스 규칙·캡슐화된 인터페이스가 여기에 해당.
  • 불변식 (Invariant) — 어떤 상황에서도 항상 참이어야 하는 조건. 예: "삼각형의 두 변의 합은 나머지 변보다 크다."
  • 계층적 복잡성 (Hierarchical Complexity) — 시스템과 하위 시스템이 중첩되며 모든 수준에서 복잡성이 발생하는 구조적 특성. 팀 버너스리가 말했듯 "모든 시스템은 다른 더 큰 시스템의 구성요소"다.
  • 분산된 큰 진흙덩이 (Distributed Big Ball of Mud) — 지역 복잡성만 줄이려다 전역 복잡성이 폭발한 마이크로서비스 안티패턴.

1. 복잡성의 본성

복잡성에는 두 가지 출처가 있다. 이 구분을 명확히 하지 않으면 제거해야 할 것을 관리하고 관리해야 할 것을 제거하려다 시간을 낭비한다.

1.1 필수적 복잡성 — 도메인이 가진 진짜 어려움

비즈니스 도메인 자체에서 비롯된다. 알고리즘 거래 시스템을 예로 들면, 다양한 시장 역학·규제 요구사항·금융 상품의 복잡성은 소프트웨어 엔지니어가 만든 게 아니라 금융이라는 도메인이 원래 그렇기 때문에 존재한다.

핵심: 제거 불가. 대신 시스템을 여러 구성요소로 분할하여 인지 부하를 줄이는 방식으로 관리한다.

1.2 우발적 복잡성 — 우리가 만든 불필요한 어려움

설계 결정을 최적으로 내리지 않았을 때의 부산물이다. 소프트웨어 엔지니어와 아키텍트로서 우발적 복잡성을 피하고 필수적 복잡성을 관리하는 것이 우리의 책임이다.

잘못된 예 — 우발적 복잡성을 양산하는 과도한 마이크로서비스 분리

// 유행이라는 이유로 필요하지도 않은 마이크로서비스를 도입
// → 네트워크 오버헤드, 분산 트랜잭션, 복잡한 배포 등 우발적 복잡성 폭증
public class OrderService {
    private readonly HttpClient _inventoryClient;  // 굳이 원격 호출?
    private readonly HttpClient _paymentClient;    // 같은 바운디드 컨텍스트인데?

    public async Task<Order> PlaceOrder(OrderRequest req) {
        var stock = await _inventoryClient.GetAsync($"/stock/{req.ItemId}");
        // 네트워크가 끊기면? 부분 실패는? 재시도는?
        var payment = await _paymentClient.PostAsync("/charge", ...);
    }
}

올바른 예 — 단일 서비스 내 메서드 호출로 충분한 경우

// 복잡성이 필요 없는 곳에 복잡성을 들이지 않는다
public class OrderService {
    private readonly InventoryRepository _inventory;
    private readonly PaymentGateway _payment;

    public Order PlaceOrder(OrderRequest req) {
        var stock = _inventory.GetStock(req.ItemId);  // 명확, 추적 가능
        var payment = _payment.Charge(req.PaymentInfo);
        return Order.Create(stock, payment);
    }
}

교훈: 우발적 복잡성은 엔지니어의 책임이다. 도구·기술을 유행 때문에 도입하거나, 필요하지 않은 추상화를 쌓거나, 경험 없는 기술 스택을 물려받는 것이 대표적 원인이다.


2. 복잡성과 시스템 설계 — 선형적 vs 복잡한 상호작용

찰스 페로의 연구(《무엇이 재앙을 만드는가?》, 알에이치코리아 2013)에 따르면, 시스템의 복잡성은 크기가 아니라 구성요소 간 상호작용의 특성에서 결정된다.

2.1 선형적 상호작용 — 기계식 시계처럼 추적 가능하다

인과관계가 명확하고 예측 가능하다. 구성요소 간의 종속성이 명확하여 어떤 구성요소에 변경을 가했을 때 다른 구성요소에 어떤 영향을 주는지 추적할 수 있다.

비유: 기계식 시계 — 수십 개의 기어와 스프링이 있지만, 특정 기어가 멈추면 어떤 바늘이 영향받는지 도식으로 따라갈 수 있다. 커네빈 관점에서 선형적 상호작용은 전문성 수준에 따라 명확 또는 복합 도메인에 속한다.

# 선형적 상호작용 예시: 명확한 의존 흐름
class OrderPipeline:
    def process(self, order: Order) -> Receipt:
        validated = self._validator.validate(order)         # 1단계
        priced    = self._pricer.apply_discount(validated)  # 2단계
        receipt   = self._payment.charge(priced)            # 3단계
        self._notifier.send_confirmation(receipt)           # 4단계
        return receipt
    # 각 단계의 입출력이 명확. 3단계가 실패하면 4단계는 실행되지 않는다.

2.2 복잡한 상호작용 — 의도치 않은 결과가 숨어 있다

두 가지 유형의 문제가 나타난다.

유형 A: 예상하지 못한 방식의 의도한 효과 시스템은 작동은 하지만 아무도 어떻게 작동하는지 모르는 상황이다.

원인: - 암묵적 지식을 가진 사람이 조직을 떠나 버림 - 우발적 복잡성으로 인한 코드 난해화 - 경험 없는 기술 스택 물려받기

유형 B: 의도치 않은 결과 목표를 달성하는 과정에서 예상치 못한 사고나 오류가 발생한다. 전형적인 패턴 3가지: 1. 한 구성요소의 변경이 다른 부분에 예상치 못한 영향을 미친다. 2. 한 구성요소의 오류가 겉보기에 관련 없는 전체 시스템에 연쇄 오류를 일으킨다. 3. 시스템 환경에 대한 암묵적 가정(예: "네트워크는 항상 안정적")이 실패한다.

# 복잡한 상호작용 예시 A: 암묵적 가정 — 분산 컴퓨팅의 오류
class ShippingService:
    def calculate_fee(self, order):
        # "네트워크는 항상 안정적"이라는 암묵적 가정
        price_data = requests.get("http://pricing-service/rates").json()
        return price_data["standard_fee"] * order.weight
        # pricing-service가 0.3초 지연되면? timeout은? 부분 응답은?
        # 이 가정은 코드 어디에도 명시되지 않아 숨어 있다.
# 복잡한 상호작용 예시 B: 멀리 떨어진 변경이 연쇄 파급
# 어떤 엔지니어가 SupportCase.OpenedOn → SupportCase.Timestamp 로 필드명 변경
# (DB 컬럼명은 opened_on 유지 — 기존 쿼리 수정 비용 때문에)
class SupportCaseReport:
    def get_old_cases(self, days):
        # SQL 쿼리에 이전 컬럼명이 박혀 있다 — 공유된 암묵적 지식
        return db.execute(
            "SELECT * FROM support_cases WHERE opened_on < ?",  # 폭발 지점
            [datetime.now() - timedelta(days=days)]
        )

복잡한 상호작용과 커네빈: 변화의 효과를 판별하는 유일한 방법이 실험이라면 복잡한 도메인이다. 인과관계가 아예 없다면 혼돈 도메인이다.

2.3 복잡성과 시스템 크기는 무관하다

중요한 통찰: 복잡성 ≠ 크기

  • 5,000개 구성요소 + 선형 상호작용 → 단순한 시스템
  • 5개 구성요소 + 복잡한 상호작용 → 복잡한 시스템 (심하면 혼돈)

시스템이 언뜻 선형적으로 보이더라도 복잡성은 여전히 표면 아래에 숨어 있을 수 있다.


3. 계층적 복잡성 — 지역 vs 전역

글렌퍼드 마이어스(《Composite/Structured Design》, 1979)의 구분을 소프트웨어에 적용한다. 팀 버너스리가 말했듯 "모든 시스템은 다른 더 큰 시스템의 구성요소"다. 따라서 시스템과 복잡성은 본질적으로 계층적이다.

복잡성 종류 무엇의 상호작용? WolfDesk 예시
전역 복잡성 구성요소 사이의 상호작용 WolfDesk 마이크로서비스들 간 통신
지역 복잡성 단일 구성요소 내부의 상호작용 사례 관리 서비스 내 객체들

WolfDesk 계층 구조 (프랙털 특성):

[WolfDesk 시스템]  ← 최상위: 마이크로서비스들이 전역으로 상호작용
    └─ [사례 관리 마이크로서비스]  ← 전역에서 보면 하나의 구성요소
            └─ [지원 사례 집합체]  ← 지역에서 보면 하나의 구성요소
                    └─ [지원 사례 / 메시지 / 첨부 개체들]

관점을 바꾸면 전역 복잡성이 지역 복잡성이 된다. 어떤 수준에서 보든 목적·구성요소·상호작용으로 분석할 수 있으며, 복잡한 상호작용은 모든 수준에서 발생할 수 있다.

핵심: 지역·전역 복잡성 중 어느 하나가 나머지보다 더 중요하지 않다. 두 가지 형태의 복잡성을 모두 해결해야 한다.

3.1 전역 복잡성만 최적화하면 — 모놀리스 함정

전역 복잡성을 0으로 만드는 가장 쉬운 방법: 모든 걸 하나의 컴포넌트로 합치면 된다. 구성요소 간 상호작용이 사라지니까.

잘못된 예

# "전역 복잡성 최소화"라는 명목으로 모든 기능을 한 클래스에
class GodService:
    def create_order(self, ...): ...
    def process_payment(self, ...): ...
    def send_email(self, ...): ...
    def update_inventory(self, ...): ...
    def generate_report(self, ...): ...
    def manage_users(self, ...): ...
    # LOC 수천 줄... 지역 복잡성 폭발. 어디서 무슨 일이 일어나는지 아무도 모른다.

결과: 전역 복잡성(구성요소 간) ↓, 지역 복잡성(내부) 폭등. 복잡성을 제거한 게 아니라 더 낮은 수준의 추상화에 집어넣은 것이다.

3.2 지역 복잡성만 최적화하면 — 분산 큰 진흙덩이 함정

"마이크로서비스 코드베이스 최대 100줄 제한" 식으로 크기만 줄이면 지역 복잡성은 낮아지지만 전역 복잡성이 치솟는다. 이것이 분산된 큰 진흙덩이 (Distributed Big Ball of Mud) 안티패턴이다.

잘못된 예

[극소 서비스 A: 주문 생성] → [극소 서비스 B: 재고 확인]
                            → [극소 서비스 C: 가격 계산]
                            → [극소 서비스 D: 할인 적용]
                            → [극소 서비스 E: 세금 계산]
                            → ...
# 서비스 하나는 100줄이지만, 서비스 간 호출 그래프가 스파게티.
# 복잡성을 크기와 동일시한 함정. 상호작용의 특성은 변하지 않는다.

3.3 균형 잡기 — 양쪽 모두 관리해야 한다

지역·전역 복잡성은 모두 결합(구성요소 간 상호작용)에서 비롯된다. 10장에서 공유 지식의 방식을 조정해 양쪽을 균형 있게 다루는 법을 배운다.


4. 자유도 — 복잡한 상호작용의 지표

"자유도(degrees of freedom)"는 역학·열역학 개념을 소프트웨어에 적용한 것이다. 다른 변수의 제약을 받지 않고 독립적으로 변할 수 있는 변수의 수다. 자유도가 높을수록 시스템 상태를 정의하는 데 더 많은 정보가 필요하고, 가능한 상호작용이 많아진다.

4.1 기본 예시 — 도형으로 이해하기

// 정사각형: 자유도 = 1 (변의 길이 하나로 완전히 정의됨)
struct Square {
    int Edge;  // 이것만 알면 넓이, 둘레 모두 계산 가능
}

// 직사각형: 자유도 = 2 (높이와 너비가 독립적으로 변할 수 있음)
struct Rectangle {
    int Width;
    int Height;  // 두 값이 서로 제약하지 않음 → 가능한 상태 무한히 많다
}

4.2 소프트웨어에서 자유도가 높아지는 원인

원인 1: 데이터 복제

Ordering DB ──── 주문 데이터 ────┐
                                   ├── 두 DB에 같은 데이터 존재 → 자유도 2
Shipping DB  ────주문 데이터 ────┘

Ordering DB의 주문 데이터와 Shipping DB의 복제본이 동기화되지 않으면 시스템은 불일치 상태가 된다. 장기간 네트워크 분리 시 Shipping 서비스가 오래된 데이터로 작업 → 의도치 않은 결과.

원인 2: 비즈니스 로직 중복

잘못된 예 — 자유도 2 (같은 규칙이 두 곳에 독립적으로 존재)

// Ordering 서비스
bool IsQualifiedForFreeShipping(Order order) {
    return order.Amount > Currency.USD(100);
}

// Shipping 서비스 (동일한 비즈니스 규칙의 독립적 사본)
bool IsQualifiedForFreeShipping(Order order) {
    return order.Amount > Currency.USD(100);
}
// 문제: 한쪽이 $100 → $150으로 바꾸면?
// 고객은 주문 시 무료 배송 혜택 → 실제 배송 시 요금 청구 (불일치!)

자유도 2인 IsQualifiedForFreeShipping의 가능한 상태: 1. 두 구현이 동일한 동작 (정상) 2. 두 구현이 서로 다른 동작 (불일치 — 오류)

상태 2가 존재하는 한, 복잡한 상호작용은 언제든 발생할 수 있다.

올바른 예 — 자유도 1 (단일 출처)

// 공유 도메인 서비스
public class ShippingPolicy {
    public static bool IsQualifiedForFreeShipping(Order order)
        => order.Amount > Currency.USD(100);  // 유일한 정의
}

// Ordering 서비스
bool IsQualifiedForFreeShipping(Order order)
    => ShippingPolicy.IsQualifiedForFreeShipping(order);

// Shipping 서비스
bool IsQualifiedForFreeShipping(Order order)
    => ShippingPolicy.IsQualifiedForFreeShipping(order);
// 규칙이 바뀌어도 한 곳만 바꾸면 된다. 자유도 = 1.

5. 복잡성과 제약

자유도를 줄이는 도구가 제약 (Constraint)이다. 비즈니스 규칙이나 불변식 같은 제약은 구성요소 간 가능한 상호작용을 제한하여 자유도를 줄인다.

커네빈 도메인 제약 특성 자유도
명확(Obvious) 제약 강함, 원인-결과 확정 낮음
복합(Complicated) 제약 있음, 전문가 분석 필요 중간
복잡(Complex) 제약 거의 없음 높음
혼돈(Chaotic) 제약 없음 최대

작업할 수 있는 제약이 적을수록 인과관계에 대한 확신이 줄고 복잡성이 커진다. 복잡한 상호작용을 길들이려면 제약이 필요하다.

5.1 예제: 삼각형으로 보는 제약

잘못된 예 — 제약 없는 삼각형, 자유도 3

class Triangle {
    public int EdgeA;
    public int EdgeB;
    public int EdgeC;
    // EdgeA=1, EdgeB=1, EdgeC=100 도 가능 → 수학적으로 유효하지 않은 삼각형
    // 이 객체로 연산하면 의도치 않은 결과 발생
}

올바른 예 — 불변식으로 자유도를 유효한 삼각형으로만 제한

class Triangle {
    public int EdgeA { get; private set; }
    public int EdgeB { get; private set; }
    public int EdgeC { get; private set; }

    public void SetEdges(int edgeA, int edgeB, int edgeC) {
        // 제약: 삼각형 불등식 (두 변의 합 > 나머지 한 변)
        if ((edgeA + edgeB) < edgeC ||
            (edgeA + edgeC) < edgeB ||
            (edgeB + edgeC) < edgeA) {
            throw new ValueException("수학적으로 유효하지 않은 삼각형");
        }
        EdgeA = edgeA;
        EdgeB = edgeB;
        EdgeC = edgeC;
    }
}
// 이제 Triangle의 모든 인스턴스는 반드시 유효한 삼각형.
// 불변식이 자유도를 '유효한 상태'로만 제한했다.

6. 결합과 복잡한 상호작용 — WolfDesk Repository 실전

결합은 설계 결정이며, 본질적으로 구성요소의 통합 제약 조건을 정의한다. 어떤 상호작용을 가능하게 하고, 어떤 상호작용을 금지할지 결정한다. 제약 조건의 개념은 결합과 복잡성을 연결하는 고리다.

주의사항: 아래 예제는 사고실험이다. 어떤 저장소 설계가 최선인지에 대한 지침이 아니라, 구성요소가 결합되는 방식이 복잡성에 어떤 영향을 주는지 보여주기 위한 것이다.

WolfDesk의 지원 사례 저장소는 지원 사례 데이터를 데이터베이스에 저장하고, 다른 구성요소에서 데이터베이스를 캡슐화하도록 설계되었다.

6.1 설계 A: SQL 직접 노출 — 최대 유연성, 최대 자유도

// 저장소 인터페이스: SQL Where 절을 직접 받는다
public interface SupportCaseRepository {
    void Insert(SupportCase c);
    void Update(SupportCase c);
    void Delete(SupportCase c);
    SupportCase GetById(CaseId id);
    IEnumerable<SupportCase> Query(
        string sqlWhere,                          // SQL 문자열 직접 노출
        Dictionary<string, object> paramValues);  // 파라미터 딕셔너리
}

// 사용 예
var paramValues = new Dictionary<string, object> {
    ["TId"] = 10,
    ["Months"] = 3
};
var q = "Tenant=@TId and OpenedOn<=dateadd(month, @Months, getdate())";
var cases = repository.Query(q, paramValues);

이 인터페이스가 노출하는 지식(= 공유하는 암묵적 가정): - DB 스키마: 열 이름이 SupportCase 객체의 필드와 일치해야 한다. - DB 인덱스: 성능 좋은 쿼리를 작성하려면 인덱스 구조를 알아야 한다. - DB 패밀리: SQL을 쓰므로 관계형 DB라고 가정한다. - DB 방언: MSSQL dateadd 같은 벤더 함수를 쓰면 다른 DB로 이전 불가.

복잡한 상호작용 시나리오 1 — 필드명 변경의 연쇄 파급:

[엔지니어 A] SupportCase.OpenedOn → SupportCase.Timestamp 로 필드명 변경
              (단, DB 컬럼은 opened_on 유지 — 기존 쿼리 수정 비용 때문에)

[엔지니어 B] 나중에 "Timestamp" 필드로 쿼리 작성
              "WHERE Timestamp < ?" → 런타임 에러! (컬럼이 없다)
              → 일반적인 규칙을 따랐는데도 의도치 않은 결과 발생

복잡한 상호작용 시나리오 2 — DB 이전 시 방언 충돌:

[팀] 확장성을 위해 Cloud Spanner로 DB 마이그레이션 결정
     → SQL 방언이 다름 → 쿼리 코드 대부분 재작성
     → SQL 인터페이스가 외부 지식을 너무 많이 공유해 이전 비용 폭증

두 시나리오의 공통점: 저장소 인터페이스가 명시적(열 이름) 및 암묵적(SQL 방언) 가정의 형태로 외부 지식 공유를 허용한다.

6.2 설계 B: Query 객체 패턴 — 중간 지점

// 저장소 인터페이스 (Query 객체 사용)
public interface SupportCaseRepository {
    void Insert(SupportCase c);
    void Update(SupportCase c);
    void Delete(SupportCase c);
    SupportCase GetById(CaseId id);
    IEnumerable<SupportCase> Query(QueryObject query);  // SQL 대신 객체
}

// 사용 예
var query = new QueryObject(typeof(SupportCase))
    .AddCriteria(Criteria.Equals("TenantId", 10))
    .AddCriteria(Criteria.LessOrEquals("OpenedOn",
        DateTime.Now.AddMonths(-3)));
var cases = repository.Query(query);

개선점: SQL 방언을 저장소 내부로 캡슐화. MongoDB로 바꾼다면 Query 구현만 MQL로 변환하면 된다. 허용되는 쿼리를 애플리케이션 요구사항을 충족하는 DB 쿼리 기능의 하위 집합으로 제한한다.

여전히 남은 문제점: - DB 스키마: Criteria.LessOrEquals("OpenedOn", ...) — 열 이름은 여전히 노출됨 - DB 인덱스: 임의의 기준 조합은 비효율적 쿼리를 만들 수 있음 → 다운타임 위험

6.3 설계 C: 전문화된 파인더 메서드 — 최대 제약, 최소 자유도

// 저장소 인터페이스 (파인더 메서드)
public interface SupportCaseRepository {
    void Insert(SupportCase c);
    void Update(SupportCase c);
    void Delete(SupportCase c);
    SupportCase GetById(CaseId id);
    // 허용되는 쿼리를 명시적 메서드로 제한
    IEnumerable<SupportCase> AllCases(TenantId tenant);
    IEnumerable<SupportCase> CreatedBefore(TenantId tenant, DateTime date);
    IEnumerable<SupportCase> MatchingStatus(TenantId tenant, Status status);
}

// 사용 예 — 훨씬 간결하고 오용 불가
var cases = repository.CreatedBefore(10, DateTime.Now.AddMonths(-3));

장점: - 열 이름이 파인더 메서드 내부에 캡슐화 → 소비자가 스키마를 몰라도 됨 - 지원하는 쿼리가 명시적 목록으로 제한 → DB 인덱스를 해당 쿼리에 맞춰 최적화 가능 - 임시(ad hoc) 쿼리 불가 → 복잡한 상호작용의 가능성 최소화

트레이드오프: 유연성 ↓. 새로운 쿼리 패턴이 필요하면 인터페이스에 메서드를 추가해야 한다. 이것이 유연성과 복잡한 상호작용 가능성 최소화 사이의 트레이드오프다.

6.4 세 가지 설계의 자유도·제약 비교

설계 A (SQL 직접)
자유도: ████████████ 최대
제약:   ░░░░░░░░░░░░ 최소
공유 지식: DB 스키마 + 방언 + 인덱스
복잡한 상호작용 가능성: 매우 높음

설계 B (Query 객체)
자유도: ████████░░░░ 중간
제약:   ████░░░░░░░░ 중간
공유 지식: DB 스키마 + 인덱스 (방언 제거)
복잡한 상호작용 가능성: 중간

설계 C (파인더 메서드)
자유도: ████░░░░░░░░ 낮음
제약:   ████████████ 최대
공유 지식: 없음 (완전 캡슐화)
복잡한 상호작용 가능성: 낮음

7. 결합과 복잡한 상호작용의 관계

결합은 구성요소를 연결하고 경계를 넘나드는 지식을 정의한다. 지식은 명시적(메서드 시그니처)일 수도 있고 암묵적(설계 가정)일 수도 있다.

외부 지식 공유 → 연결된 구성요소를 함께 변경해야 할 가능성 ↑
암묵적 지식   → 연쇄 변경의 필요성을 예측하기 더 어려움 ↑
                → 복잡한 상호작용 가능성 ↑

결합 설계의 목표: 원하는 상호작용은 허용하고, 복잡한 상호작용은 금지하는 방식으로 공유 지식을 제한한다.

주의: 모든 제약이 바람직한 것은 아니다. "큰 진흙덩이" 시스템은 모든 구성요소가 너무 밀접하게 결합되어 어떤 변경도 기능을 손상시킨다 — 이것도 제약이지만 원하는 제약이 아니다. 결합 설계의 핵심은 어떤 상호작용을 허용할지 의도적으로 선택하는 것이다.


핵심 개념 정리

개념 한 줄 설명
필수적 복잡성 도메인 자체에 내재. 제거 불가, 관리 대상
우발적 복잡성 설계 부주의 산물. 제거 가능, 엔지니어 책임
선형적 상호작용 인과관계 명확. 변경 영향 추적 가능. 커네빈 명확/복합 도메인
복잡한 상호작용 인과관계 불명확. 의도치 않은 결과. 커네빈 복잡/혼돈 도메인
지역 복잡성 단일 구성요소 내부 상호작용의 복잡성
전역 복잡성 구성요소 간 상호작용의 복잡성
자유도 독립적으로 변할 수 있는 변수의 수. 높을수록 복잡한 상호작용 가능성 ↑
제약 가능한 상태/상호작용을 제한하는 규칙. 자유도를 줄인다
결합 구성요소 통합의 제약 조건 설계. 허용/금지 상호작용을 결정
파인더 메서드 허용되는 쿼리를 명시적 메서드로 제한 → 최소 자유도
Query 객체 패턴 SQL을 객체로 추상화 → 중간 수준 제약
분산 큰 진흙덩이 지역 복잡성만 줄이다 전역 복잡성이 폭발한 상태

실무 체크리스트

  • [ ] 지금 다루는 복잡성이 필수적(도메인 자체)인가, 우발적(설계 부주의)인가 구분했는가?
  • [ ] 시스템 변경 시 영향을 받는 모든 구성요소를 추적할 수 있는가? 추적 불가라면 복잡한 상호작용의 신호다.
  • [ ] 새로운 아키텍처(마이크로서비스 분리 등)가 지역 복잡성만 줄이고 전역 복잡성을 늘리지는 않는가?
  • [ ] 코드베이스에 동일한 비즈니스 규칙이 둘 이상의 장소에 구현되어 있는가? → 자유도 증가 위험.
  • [ ] 저장소 인터페이스가 DB 스키마·방언·인덱스 구조를 소비자에게 노출하는가? 노출할수록 결합이 강해진다.
  • [ ] 제약이 원하는 상호작용은 허용하고 원하지 않는 상호작용은 금지하도록 설계되어 있는가?
  • [ ] 시스템이 더 커지거나 몇 년 후에도 변경 결과를 예측할 수 있겠는가? 예측 불가라면 복잡한 상호작용 가능성이 높다.
  • [ ] 모듈·서비스 분리 시 크기가 아니라 상호작용의 특성을 기준으로 결정했는가?

연습문제 (정답은 부록 D)

  1. 개념. 알고리즘 트레이딩 시스템에서 "(a) 다양한 금융 상품의 복잡한 가격 계산 규칙"과 "(b) 팀이 유행을 따라 도입한 과도한 마이크로서비스 분리"를 필수적 복잡성과 우발적 복잡성으로 각각 분류하고, 각각에 대한 처방을 한 문장으로 제시하라.

  2. 분석. 다음 두 시스템을 선형적 vs 복잡한 상호작용 관점에서 비교하라. (a) 100만 개의 부품이 있지만 각 부품이 오직 하나의 다음 부품에만 영향을 주는 생산 라인. (b) 10개의 서비스가 있지만 서로가 서로를 직접 호출하는 완전 연결(full mesh) 네트워크. 어느 쪽이 더 복잡한가, 그 이유는?

  3. 계산. 다음 클래스의 자유도를 계산하고, 비즈니스 로직 중복이 어떻게 자유도를 높이는지 설명하라. 이 자유도를 1로 줄이려면 어떻게 해야 하는가? csharp class PricingService { public decimal CalculateTax(decimal amount) => amount * 0.1m; } class InvoiceService { public decimal CalculateTax(decimal amount) => amount * 0.1m; }

  4. 설계. 전자상거래 플랫폼의 주문 저장소를 설계한다. 현재 MySQL을 쓰고 있지만 6개월 후 DynamoDB로 이전 예정이다. 본문의 세 가지 설계(SQL 직접·Query 객체·파인더 메서드) 중 어느 것을 선택하겠는가? 자유도·제약·유연성의 트레이드오프를 들어 설명하라.

  5. 트레이드오프. 팀이 모놀리식 시스템을 100개의 극소 마이크로서비스로 분해했다. 각 서비스는 50줄 이하다. 이 결정이 지역·전역 복잡성에 각각 어떤 영향을 미쳤는지 설명하고, 이 접근 방식의 근본적인 오류를 지적하라.


최신 동향 (검증 2026-05-26)

  • 복잡성 측정 도구 성숙: 정적 분석·의존 그래프 시각화 도구가 "전역 vs 지역 복잡성"을 자동으로 정량화하는 방향으로 발전 중이다. CodeScene·Lattix 같은 도구가 컴포넌트 간 결합도와 핫스폿 복잡성을 분리해 보고한다.

  • 모듈리스(Modulith) 패턴 대두: 지역 복잡성(단일 서비스)과 전역 복잡성(서비스 간 결합)의 균형 문제에 대응하여, 단일 배포 단위 안에서 강한 모듈 경계를 유지하는 Spring Modulith 등의 접근법이 주목받고 있다 — 지역·전역 복잡성 균형의 현대적 구현 사례.

  • 찰스 페로 "정상 사고" 이론의 소프트웨어 재해석: 분산 시스템 장애 분석에 페로의 연구를 적용하는 SRE 실천이 확산되고 있다. 복잡한 상호작용과 밀결합이 대규모 장애의 공통 원인임을 실증 데이터로 뒷받침한다.


부록 A. 용어 사전

한글 용어 원문 영문명 의미
필수적 복잡성 Essential Complexity 비즈니스 도메인 자체에 내재된 불가피한 복잡성
우발적 복잡성 Accidental Complexity 최적화되지 않은 설계 결정으로 발생한 불필요한 복잡성
선형적 상호작용 Linear Interaction 원인과 결과가 명확하고 예측 가능한 구성요소 간 상호작용
복잡한 상호작용 Complex Interaction 인과관계가 불분명하고 의도치 않은 결과를 초래하는 상호작용
지역 복잡성 Local Complexity 단일 구성요소 내부 상호작용의 복잡성
전역 복잡성 Global Complexity 시스템 구성요소들 사이 상호작용의 복잡성
자유도 Degrees of Freedom 독립적으로 변할 수 있는 변수의 수
제약 Constraint 가능한 상태 또는 상호작용을 제한하는 규칙
불변식 Invariant 어떤 상황에서도 항상 참이어야 하는 조건
파인더 메서드 Finder Method 허용되는 쿼리 패턴을 명시적 메서드로 표현한 저장소 설계
Query 객체 Query Object SQL 쿼리를 객체 구조로 추상화하는 패턴
계층적 복잡성 Hierarchical Complexity 시스템 내 모든 수준에서 발생하는 복잡성
분산 큰 진흙덩이 Distributed Big Ball of Mud 지역 복잡성만 최소화한 결과 전역 복잡성이 폭발한 마이크로서비스 안티패턴

부록 B. 핵심 비교표

필수적 복잡성 vs 우발적 복잡성

구분 필수적 복잡성 우발적 복잡성
출처 비즈니스 도메인 자체 설계 결정 부주의
제거 가능 여부 제거 불가 제거 가능
처방 신중한 설계로 관리 더 나은 설계로 제거
책임 도메인의 본질 엔지니어의 책임
예시 금융 규제·시장 역학 복잡성 불필요한 마이크로서비스·중복 비즈니스 로직

선형적 상호작용 vs 복잡한 상호작용

구분 선형적 상호작용 복잡한 상호작용
인과관계 명확, 추적 가능 불분명, 추적 불가
예측 가능성 높음 낮음 (실험 필요)
변경 영향 미리 알 수 있음 실험 없이 알 수 없음
커네빈 도메인 명확/복합 복잡/혼돈
비유 기계식 시계 암묵적 네트워크 가정
대응 전략 현재 설계 유지 제약 강화, 자유도 감소

지역 복잡성 vs 전역 복잡성

구분 지역 복잡성 전역 복잡성
관찰 대상 단일 구성요소 내부 구성요소들 사이
WolfDesk 예시 사례 관리 서비스 내 객체 상호작용 마이크로서비스 간 통신
최소화 실수 모든 것을 하나의 컴포넌트로 합침 100줄 제한 극소 마이크로서비스
최소화 결과 전역 복잡성 폭등 분산 큰 진흙덩이
중요도 둘 다 똑같이 중요 둘 다 똑같이 중요

Repository 설계 세 가지 비교 (WolfDesk 예시)

설계 자유도 제약 공유 지식 유연성 복잡한 상호작용 위험
A: SQL 직접 최대 최소 DB 스키마·방언·인덱스 최대 최고
B: Query 객체 중간 중간 DB 스키마·인덱스 (방언 제거) 중간 중간
C: 파인더 메서드 최소 최대 없음 (완전 캡슐화) 최소 최저

부록 C. 추천 참고 자료

Tier 1 공식·표준 (생존 확인 2026-05-26)

자료 링크
책 공식 (제이펍) jpub.kr
원서 출판사 — Manning Balancing Coupling in Software Design
찰스 페로 — 《무엇이 재앙을 만드는가?》 Normal Accidents (위키)
분산 컴퓨팅의 오류 en.wikipedia.org/wiki/Fallacies_of_distributed_computing
마틴 파울러 — DDD Aggregate martinfowler.com/bliki/DDD_Aggregate.html
팀 버너스리 — Principles of Design w3.org/DesignIssues/Principles.html
Spring Modulith spring.io/projects/spring-modulith

책 다른 장 안내

설명
1장 결합의 정의, 공유 수명주기·공유 지식
2장 커네빈 프레임워크로 복잡성 분류
4장 모듈성 — 결합의 반대편 (활성화하려는 자유도)
10장 결합 균형 — 지역·전역 복잡성을 동시에 관리

부록 D. 연습문제 풀이

  1. (a) 필수적 복잡성 — 처방: 도메인을 여러 구성요소로 분할하여 인지 부하를 줄인다. (b) 우발적 복잡성 — 처방: 마이크로서비스 분리를 실제 필요성에 근거할 때만 도입한다. 금융 가격 계산 규칙은 도메인 자체의 요구사항이므로 필수적 복잡성이다. 유행을 따른 마이크로서비스 분리는 엔지니어의 설계 결정에서 비롯된 우발적 복잡성이다.

  2. (b) 10개 서비스 완전 연결 네트워크가 더 복잡하다. 생산 라인(a)은 구성요소가 100만 개지만 각 구성요소의 영향을 선형으로 추적할 수 있다. 완전 연결 네트워크(b)는 서비스 10개가 서로 직접 호출하면 최대 10×9=90개의 호출 경로가 생긴다. 한 서비스의 장애가 모든 연결 서비스에 연쇄 영향을 줄 수 있어 인과관계 추적이 불가능하다 — 전형적인 복잡한 상호작용이다.

  3. (자유도 = 2. 비즈니스 로직 중복이 자유도를 높인다.) CalculateTax가 두 클래스에 독립적으로 구현되어 있으므로, 두 구현의 세율이 같은 상태다른 상태 두 가지가 가능하다 — 자유도 2. 한쪽이 10%를 유지하는 동안 다른 쪽이 11%로 변경되면 시스템은 불일치 상태가 된다. 단일 TaxPolicy 클래스로 통합하면 자유도 1로 줄어든다.

  4. (설계 C — 파인더 메서드 권장) DB 이전이 예정되어 있으므로 SQL을 직접 노출하는 설계 A는 이전 시 쿼리 대부분을 재작성해야 한다. Query 객체(B)는 SQL 방언을 캡슐화하지만 스키마 노출은 남는다. 파인더 메서드(C)는 쿼리 가능성을 제한하는 대신 스키마를 완전히 캡슐화하여 DynamoDB로의 이전 시 저장소 구현만 교체하면 된다. 이전 일정이 정해진 상황에서는 단기 유연성 손실보다 이전 비용 최소화가 더 중요하므로 C를 선택한다.

  5. (지역 복잡성 ↓, 전역 복잡성 폭발. 근본 오류: 복잡성을 크기와 동일시한 것.) 각 서비스 50줄은 지역적으로는 이해하기 쉽다. 그러나 100개 서비스가 협력해야 하는 기능을 구현하면 서비스 간 호출 그래프가 폭발하고, 한 서비스의 장애가 연쇄 장애로 이어지며, 분산 트랜잭션 조율이 필요해진다 — "분산 큰 진흙덩이"다. 근본 오류는 복잡성 = 크기라는 잘못된 가정이다. 복잡성은 구성요소의 크기가 아니라 상호작용의 특성에서 결정된다. 올바른 분리 기준은 코드 줄 수가 아니라 경계를 넘는 지식의 양과 상호작용의 선형성이어야 한다.

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플